package org.oy.sealogger.device.gps;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.Format;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;

import org.apache.log4j.Logger;
import org.oy.sealogger.common.Config;
import org.oy.sealogger.common.Messages;
import org.oy.sealogger.device.Device;
import org.oy.sealogger.ds.InputAnalyser;

/**
 * 
 * @author Juho Mattila
 *
 */
public class GPSSimulatorDevice implements Device, Runnable {
	
	private InputStream in;
	private OutputStream out;
	
	private boolean running = false;
	
	private long lastUsedTime;
	private Date d = new Date();
	//private String outputLine;
	
	private boolean connected = false;
	private Logger logger;
	
	private String time;
	
	// RMC data variables
	private double latitude = 65.03;
	private double longitude = 25.25;
	private double speed = 10;
	private double angle;
	private String date;
	
	
	
	/**
	 * class constructor
	 */
	public GPSSimulatorDevice() {
		
		// get current time
		lastUsedTime = d.getTime();
		
		logger = Config.getLogger();
	}
	

	public void connect() throws GPSDeviceException {
		
		logger.debug("Starting GPS Simulator");
		this.start();
		
		try {
			out = new PipedOutputStream();
			in = new PipedInputStream((PipedOutputStream)out);
			
			connected = true;
			
		} catch(IOException e) {
			logger.error(Messages.getMessage("error.cannot_read"), e);
			try {
				in.close();
				out.close();
			} catch (IOException e1) {
				logger.error(Messages.getMessage("error.cannot_close"), e1);
				throw new GPSDeviceException(e1.getMessage());
			}
			throw new GPSDeviceException(e.getMessage());
		}
	}
	
	public void disconnect() throws GPSDeviceException {
		try {
			in.close();
			out.close();
		} catch(IOException ioe) {
			logger.error(Messages.getMessage("error.cannot_close"), ioe);
		} 
		
		connected = false;
		this.stop();
	}
	

	public InputStream getInputStream() {
		return in;
	}

	public boolean isConnected() {
		return connected;
	}
	
	/**
	 * Creates new thread and enables running
	 *
	 */
	private void start() {
		
		running = true;
		Thread t = new Thread(this);
		t.start();
		
	}
	
	/**
	 * Stops running
	 *
	 */
	private void stop() {
		running = false;
	}
	
	/**
	 * Generates NMEA0183 data
	 */
	public void run() {
		while (running) 
		{
			
			// Static lines of NMEA0183 data line
			String nmeaGGAData = new String("$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\n");
			String nmeaGSAData = new String("$GPGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39\n");
			String nmeaGSVData = new String("$GPGSV,2,1,08,01,40,083,46,02,17,308,41,12,07,344,39,14,22,228,45*75\n");
			
			// Create NMEA0183 RMC line
			String nmeaRMCData = new String(generateRMCDataLine());
			
			// Output data lines
	
			// Output GGA Data
			outputNMEA0183Data(nmeaGGAData);
			
			try {
				Thread.sleep(250);
			} catch (InterruptedException ie) {
				logger.error(Messages.getMessage("error.unexpected_thread_interrupt"), ie);
			}
			
			// Output GSA Data
			outputNMEA0183Data(nmeaGSAData);
			
			try {
				Thread.sleep(250);
			} catch (InterruptedException ie) {
				logger.error(Messages.getMessage("error.unexpected_thread_interrupt"), ie);
			}
			
			// Output GSV Data
			outputNMEA0183Data(nmeaGSVData);
			
			try {
				Thread.sleep(250);
			} catch (InterruptedException ie) {
				logger.error(Messages.getMessage("error.unexpected_thread_interrupt"), ie);
			}
			
			// Output RMC Data
			outputNMEA0183Data(nmeaRMCData);
			
			try {
				Thread.sleep(250);
			} catch (InterruptedException ie) {
				logger.error(Messages.getMessage("error.unexpected_thread_interrupt"), ie);
			}
		}
	}
	
	/**
	 * Generates RMC line for NMEA0183 data
	 * 
	 * @return string
	 */
	private String generateRMCDataLine() {
		
		// Detertemine angel based on location, so that route of the boat is triangle
		if( longitude >= 25.25 && latitude <= 65.03) {
			
			angle = 276.71;
		
		} else if( longitude < 25.08  && latitude > 65.05) {
			angle = 81.87;
		
		} else if( longitude > 25.22 && latitude > 65.07 ) {
			
			angle = 143.13;
		}
		
		// Alter speed from -1 to 1
		Random r = new Random();
		speed = speed + ( (r.nextDouble() - 0.5) * 2 );
		
		// Make sure that boat never stops
		if( speed <= 0 ) speed = 10;
		
		// Make sure the boat doesn't exceed speeds unlikely for a sailboat
		if( speed > 20) speed = 10;
		
		
		// Calculate new location
		calculateLocation();
		
		// Convert location to degrees + minutes
		int latitudeDegrees = (int)latitude;
		int longitudeDegrees = (int)longitude;
		
		double latitudeMinutes = (latitude - latitudeDegrees) * 60;
		double longitudeMinutes = (longitude - longitudeDegrees) * 60;
		
		
		// Apply decimal separator format
		DecimalFormatSymbols dfs = new DecimalFormatSymbols();
		dfs.setDecimalSeparator('.');
		
		// Apply decimal format of minutes of degrees
		DecimalFormat dfminutes = new DecimalFormat("00.000");
		dfminutes.setDecimalFormatSymbols(dfs);
		
		// Apply decimal format of latitude
		DecimalFormat dfla = new DecimalFormat("00");
		dfla.setDecimalFormatSymbols(dfs);
		
		// Apply decimal format of longitude
		DecimalFormat dflo = new DecimalFormat("000");
		dflo.setDecimalFormatSymbols(dfs);
		
		// Apply decimal format of speed and angle
		DecimalFormat dfsa = new DecimalFormat("000.0");
		dfsa.setDecimalFormatSymbols(dfs);
				
		// Get time of last RMC line (hours minutes seconds)
		Format ft = new SimpleDateFormat("HHmmss");
		time = ft.format(lastUsedTime);
		
		// Get date of last RMC line (days months years)
		Format fd = new SimpleDateFormat("ddMMyy");
		date = fd.format(lastUsedTime);
		
		// The whole RMC line "$GPRMC,time,status,latitude,longitude,speed,angle,date,variation,checksum
		String rmcLine = new String("$GPRMC," + time + ",A," + dfla.format(latitudeDegrees) + dfminutes.format(latitudeMinutes) + ",N," + dflo.format(longitudeDegrees) + dfminutes.format(longitudeMinutes) + ",E," + dfsa.format(speed) + "," + dfsa.format(angle)+ "," + date + ",003.1,W*6A\n");
		
		
		return rmcLine;
	}
	
	/**
	 * Output given data
	 * @param String outputted data line
	 * 
	 */
	private void outputNMEA0183Data(String outputLine) {
		
		// write line to out buffer
		try {
			out.write(outputLine.getBytes());
		} catch (IOException e) {
			e.printStackTrace();
		}
			
	}		

	/**
	 * Calculates new location
	 *
	 */
	private void calculateLocation() {
		
		// calculate distance from last location
		Date date = new Date();
		long currentTime = date.getTime();
		
		double distanceInNauticalMiles = speed * ( ((double)(currentTime - lastUsedTime) / 1000) / 3600  );
		// FIXME remove the division by 60 to get grossly exaggerated speed (for testing / demonstrations)
		double distance = distanceInNauticalMiles / 2;
		//double distance = distanceInSeaMiles;
		lastUsedTime = currentTime;
		
		
		// calculate new location using trigonometry
		
		if( 0 <= angle && angle < 90 ) { 
			
			longitude = longitude + (Math.sin(Math.toRadians(angle)) * distance);
			latitude = latitude + (Math.cos(Math.toRadians(angle)) * distance);
			
		
		} else if( 90 <= angle && angle < 180   ) {
			
			double trigonometricAngle = angle - 90;
			
			longitude = longitude + (Math.cos(Math.toRadians(trigonometricAngle)) * distance);
			latitude = latitude - (Math.sin(Math.toRadians(trigonometricAngle)) * distance);
		
		} else if( 180 <= angle && angle < 270   ) {
			
			double trigonometricAngle = angle - 180;
			
			longitude = longitude - (Math.sin(Math.toRadians(trigonometricAngle)) * distance);
			latitude = latitude - (Math.cos(Math.toRadians(trigonometricAngle)) * distance);
		
		} else if( 270 <= angle && angle < 360   ) {
			
			double trigonometricAngle = angle - 270;
			
			longitude = longitude - (Math.cos(Math.toRadians(trigonometricAngle)) * distance);
			latitude = latitude + (Math.sin(Math.toRadians(trigonometricAngle)) * distance);
		}
		
	}


	public boolean dataAvailable() {
		return true;
	}

	public boolean test() {
		return true;
	}

	/**
	 * Dummy method as no input analyser is used with GPSSimulatorDevice
	 */
	public void setInputAnalyser(InputAnalyser analyser) {}


	public String getDeviceName() {
		return "GPS Simulator";
	}

}
